﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Linq;
using System.Windows.Controls;
using ChessKit.Common;

namespace ChessKit.Ui
{
  public class BoardControl : Control
  {
    #region ' Private BoardPanel Property '

    private SlaveBoardPanel _boardPanel;

    private SlaveBoardPanel BoardPanel
    {
      get { return _boardPanel; }
      set
      {
        if (_boardPanel == value) return;
        if (_boardPanel != null)
        {
          _pieces.Dispose();
        }
        _boardPanel = value;
        if (_boardPanel != null)
        {
          _pieces = new PiecesController(_boardPanel, () => new PieceControl());
          SetupDisposition();
        }
      }
    }

    #endregion

    #region ' Location --> Piece (map) '

    /// <summary>Guaranties that map is synchronous to the panel and 
    ///   BoardPanel.Location</summary>
    private class PiecesController : IDisposable
    {
      private class Sequence
      {
        public HashSet<PieceControl> WitedAnimations { get; private set; }
        public Queue<PieceControl> PendingRemove { get; private set; }
        public Queue<PieceControl> PendingAdd { get; private set; }

        public Sequence()
        {
          PendingRemove = new Queue<PieceControl>();
          PendingAdd = new Queue<PieceControl>();
          WitedAnimations = new HashSet<PieceControl>();
        }
        public bool Pending
        {
          get { return PendingAdd.Count > 0 || PendingRemove.Count > 0; }
        }
      }
      private readonly Panel _panel;
      private readonly Func<PieceControl> _factory;
      private Sequence _currentSeqeuence;
      private readonly List<Sequence> _sequences = new List<Sequence>();

      private readonly Dictionary<BoardLocation, PieceControl>
        _locationToPiece = new Dictionary<BoardLocation, PieceControl>();

      public PiecesController(Panel panel, Func<PieceControl> factory)
      {
        _panel = panel;
        _factory = factory;
        _panel.AddHandler(SlaveBoardPanel.AnimationCompletedEvent,
          new RoutedEventHandler(OnAnimationCompleted));
      }

      public void Dispose()
      {
        _panel.Children.Clear();
        _panel.RemoveHandler(SlaveBoardPanel.AnimationCompletedEvent,
          new RoutedEventHandler(OnAnimationCompleted));
      }
      public void SetupPiece(BoardLocation location, IPiece value)
      {
        var piece = _factory();
        Ui.BoardPanel.SetLocation(piece, location);
        piece.Content = value;
        _locationToPiece[location] = piece;

        if (_currentSeqeuence != null)
        {
          _currentSeqeuence.PendingAdd.Enqueue(piece);
        }
        else
        {
          _panel.Children.Add(piece);
        }
      }
      private PieceControl GetControl(BoardLocation location)
      {
        PieceControl res;
        return _locationToPiece.TryGetValue(location, out res) ? res : null;
      }
      public IPiece Get(BoardLocation location)
      {
        PieceControl res;
        return _locationToPiece.TryGetValue(location, out res) ? (IPiece)res.Content : null;
      }
      public void Remove(BoardLocation location)
      {
        var piece = GetControl(location);
        if (piece == null) return;
        _locationToPiece.Remove(location);

        if (_currentSeqeuence != null)
        {
          _currentSeqeuence.PendingRemove.Enqueue(piece);
        }
        else
        {
          _panel.Children.Remove(piece);
        }
      }

      public void Move(BoardLocation from, BoardLocation to)
      {
        var piece = GetControl(from);
        if (piece == null)
          throw new InvalidOperationException();
        if (_locationToPiece.ContainsKey(to))
          throw new InvalidOperationException();
        _locationToPiece.Remove(from);
        Ui.BoardPanel.SetLocation(piece, to);
        _locationToPiece[to] = piece;
        if (_currentSeqeuence != null && piece.Parent != null)
        {
          _currentSeqeuence.WitedAnimations.Add(piece);
        }
      }

      public IDisposable SetSequence()
      {
        _currentSeqeuence = new Sequence();
        return Disposable.Create(() =>
          {
            if (_currentSeqeuence.Pending)
            {
              if (_currentSeqeuence.WitedAnimations.Count != 0)
                _sequences.Add(_currentSeqeuence);
              else
                DoPendingStuff(_currentSeqeuence);
            }
            _currentSeqeuence = null;
          });
      }

      private void DoPendingStuff(Sequence sequence)
      {
        foreach (var piece in sequence.PendingRemove)
        {
          _panel.Children.Remove(piece);
        }
        foreach (var piece in sequence.PendingAdd)
        {
          _panel.Children.Add(piece);
        }
      }

      private void OnAnimationCompleted(object sender, RoutedEventArgs e)
      {
        var pieceControl = e.OriginalSource as PieceControl;
        if (pieceControl == null) return;

        foreach (var sequence in _sequences.ToArray())
          if (sequence.WitedAnimations.Remove(pieceControl))
            if (sequence.WitedAnimations.Count == 0)
            {
              _sequences.Remove(sequence);
              DoPendingStuff(sequence);
            }
      }
    }

    private PiecesController _pieces;

    #endregion

    #region ' Board Property '

    public static readonly DependencyProperty BoardProperty =
      DependencyProperty.Register("Board", typeof(IDispositionProvider),
      typeof(BoardControl), new PropertyMetadata(null, OnBoardPropertyChanged));

    private static void OnBoardPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var boardControl = ((BoardControl)d);
      if (e.OldValue != null)
      {
        var disposition = (IDispositionProvider)e.OldValue;
        disposition.Changed -= boardControl.OnDispositionChanged;
      }
      if (e.NewValue != null)
      {
        var disposition = (IDispositionProvider)e.NewValue;
        disposition.Changed += boardControl.OnDispositionChanged;
        boardControl.SetupDisposition();
      }
    }

    private void OnDispositionChanged(object sender, BoardMovesEventArgs e)
    {
      Update(e.Moves.ToList());
    }

    public IDispositionProvider Board
    {
      get { return (IDispositionProvider)GetValue(BoardProperty); }
      set { SetValue(BoardProperty, value); }
    }

    #endregion

    #region ' Initialization '

    static BoardControl()
    {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardControl),
        new FrameworkPropertyMetadata(typeof(BoardControl)));
    }

    public override void OnApplyTemplate()
    {
      BoardPanel = this.FindVisualChildren<SlaveBoardPanel>().FirstOrDefault();
      base.OnApplyTemplate();
    }

    #endregion

    private void SetupDisposition()
    {
      if (BoardPanel == null) return;
      if (Board == null) return;
      BoardPanel.Children.Clear();
      foreach (var location in BoardLocation.Cells)
      {
        var piece = Board.GetPiece(location);
        if (piece != null) _pieces.SetupPiece(location, piece);
      }
    }
    private void Update(IEnumerable<BoardLocation[]> moves)
    {
      using (_pieces.SetSequence())
        foreach (var batch in moves)
          switch (batch.Length)
          {
            case 2:
              _pieces.Remove(batch[1]);
              _pieces.Move(batch[0], batch[1]);
              UpdateLocation(batch[0]); // For the case of promotion
              UpdateLocation(batch[1]); // For the case it's move undo
              break;
            case 1:
              UpdateLocation(batch[0]);
              break;
          }
    }

    private void UpdateLocation(BoardLocation location)
    {
      var expected = Board.GetPiece(location);
      var actual = _pieces.Get(location);
      if (expected == actual) return;
      if (expected == null)
      {
        _pieces.Remove(location);
      }
      else if (actual == null)
      {
        _pieces.SetupPiece(location, expected);
      }
      else
      {
        _pieces.Remove(location);
        _pieces.SetupPiece(location, expected);
      }
    }
  }
}
